package com.yangrd.helper.crudhelper.core.crud.ctrl.impl;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yangrd.helper.crudhelper.common.jpa.AbstractPersistable;
import com.yangrd.helper.crudhelper.common.util.ReflectionUtils;
import com.yangrd.helper.crudhelper.core.EmbeddedSelect;
import com.yangrd.helper.crudhelper.core.crud.ctrl.CrudCtrlService;
import com.yangrd.helper.crudhelper.core.crud.ctrl.SpecHelper;
import com.yangrd.helper.crudhelper.core.crud.page.DomainMappingManager;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.*;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import javax.persistence.EntityNotFoundException;
import javax.servlet.http.HttpServletRequest;
import java.beans.PropertyDescriptor;
import java.lang.reflect.*;
import java.util.*;
import java.util.stream.Collectors;

/**
 * description: JpaRepositoryCrudCtrlService
 *
 * @author: yangrd
 * @date: 2020/10/23
 * @version: 1.0
 */
@RequiredArgsConstructor
public class JpaRepositoryCrudCtrlService implements CrudCtrlService {

    private final DomainMappingManager domainMappingManager;

    private final ObjectMapper objectMapper;

    private final SpecHelper specHelper;

    private TypeConverterSupport typeConverterSupport = new SimpleTypeConverter();

    @Override
    public Object save(String domainName, String reqBody) {
        return domainMappingManager.getJpaRepository(domainName).map(repo -> {
            Class<?> domainClass = domainMappingManager.getDomainClass(domainName);
            Object o = toObj(reqBody, domainClass);
            onionEmbedded(domainClass, o);
            repo.saveAndFlush(o);
            return o;
        }).orElseThrow(RuntimeException::new);
    }

    @Override
    public void delete(String domainName, List<String> ids) {
        domainMappingManager.getJpaRepository(domainName).ifPresent(repo -> {
            ids.stream().map(id -> toId(id, domainName)).forEach(repo::deleteById);
        });
    }

    @Override
    public void update(String domainName, String id, String reqBody) {
        domainMappingManager.getJpaRepository(domainName).ifPresent(repo -> {
            repo.findById(toId(id, domainName)).ifPresent(db -> {
                Object req = toObj(reqBody, db.getClass());
                BeanUtils.copyProperties(req, db);
                onionEmbedded(db.getClass(), db);
                repo.saveAndFlush(db);
            });
        });
    }

    @Override
    public List<Object> listAll(String domainName, HttpServletRequest request, Sort sort) {
        return domainMappingManager.getJpaRepository(domainName).map(repo -> {
            if (repo instanceof JpaSpecificationExecutor) {
                return ((JpaSpecificationExecutor) repo).findAll(specHelper.toSpec(domainMappingManager.getDomainClass(domainName), request), sort);
            } else {
                return repo.findAll(sort);
            }
        }).orElseThrow(EntityNotFoundException::new);
    }

    @Override
    public Page<Object> findAll(String domainName, HttpServletRequest request, Pageable pageable) {
        return domainMappingManager.getJpaRepository(domainName).map(repo -> {
            if (repo instanceof JpaSpecificationExecutor) {
                return ((JpaSpecificationExecutor) repo).findAll(specHelper.toSpec(domainMappingManager.getDomainClass(domainName), request), pageable).map(o -> {
                    warpParams(o);
                    return o;
                });
            } else {
                return repo.findAll(pageable).map(o -> {
                    warpParams(o);
                    return o;
                });
            }
        }).orElseThrow(EntityNotFoundException::new);
    }

    @Override
    public Object get(String domainName, String id) {
        Object domain = domainMappingManager.getJpaRepository(domainName).map(repo -> repo.findById(toId(id, domainName)).get()).orElseThrow(EntityNotFoundException::new);
        warpParams(domain);
        return domain;
    }

    private Object toObj(String json, Class<?> clazz) {
        Object o = null;
        try {
            o = objectMapper.readValue(json, clazz);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return o;
    }

    private void warpParams(Object domain) {
        Class<?> domainClass = domain.getClass();
        ReflectionUtils.listAllAnnotationField(domainClass, EmbeddedSelect.class).forEach(field -> {
            field.setAccessible(true);
            if (domain instanceof AbstractPersistable) {
                Map<String, Object> params = ((AbstractPersistable) domain).getParams();
                if (Objects.isNull(params)) {
                    params = new HashMap<>();
                    field.setAccessible(true);

                    Optional<Field> optionalField = ReflectionUtils.getFiled(domainClass, "params");
                    if (optionalField.isPresent()) {
                        Field field1 = optionalField.get();
                        field1.setAccessible(true);
                        try {
                            field1.set(domain, params);
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    }

                }
                Map<String, Object> fParams = new HashMap<>(3);
                params.put(field.getName(), fParams);
                if (Collection.class.isAssignableFrom(field.getType())) {
                    try {
                        Object o = field.get(domain);
                        Collection<Object> objectList = (Collection<Object>) o;
                        List<String> ids = new ArrayList<>();
                        objectList.forEach(o1 -> {
                            ReflectionUtils.getFiled(o1.getClass(), "id").ifPresent(field1 -> {
                                ids.add(ReflectionUtils.getFieldVal(field1, o1).toString());
                            });
                        });
                        fParams.put("ids", ids.stream().collect(Collectors.joining(",")));
                        List<String> names = new ArrayList<>();
                        objectList.forEach(o1 -> {
                            ReflectionUtils.getFiled(o1.getClass(), "name").ifPresent(field1 -> {
                                names.add(ReflectionUtils.getFieldVal(field1, o1).toString());
                            });
                        });
                        fParams.put("name", names.stream().collect(Collectors.joining(",")));
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                } else {
                    Optional<Field> optionalField = ReflectionUtils.getFiled(field.getType(), "id");
                    if (optionalField.isPresent()) {
                        try {
                            Object fieldVal = ReflectionUtils.getFieldVal(optionalField.get(), field.get(domain));
                            fParams.put("id", fieldVal);
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    }
                    Optional<Field> optionalField1 = ReflectionUtils.getFiled(field.getType(), "name");
                    if (optionalField1.isPresent()) {
                        try {
                            Object fieldVal = ReflectionUtils.getFieldVal(optionalField1.get(), field.get(domain));
                            fParams.put("name", fieldVal);
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    }
                }

            }
        });
    }

    private Object toId(String id, String domain) {
        return typeConverterSupport.convertIfNecessary(id, domainMappingManager.getIdClass(domain));
    }

    private void onionEmbedded(Class<?> domainClass, Object domain) {
        ReflectionUtils.listAllAnnotationField(domainClass, EmbeddedSelect.class).forEach(field -> {
            String name = field.getName();
            if (domain instanceof AbstractPersistable) {
                if (Collection.class.isAssignableFrom(field.getType())) {
                    Map<String, Object> params = ((AbstractPersistable) domain).getParams();
                    if (Objects.nonNull(params)) {
                        if (params.containsKey(name)) {
                            Object o = params.get(name);
                            if (o instanceof Map) {
                                Object idsStr = ((Map) o).get("ids");
                                onionItems(domain, field, Arrays.asList(idsStr.toString().split(",")));
                            }
                        }
                    }
                } else {
                    onionItem(domain, field, name);
                }
            }
        });
    }

    private void onionItems(Object domain, Field field, List<String> ids) {
        Type genericType = field.getGenericType();
        if (genericType instanceof ParameterizedType) {
            Type rawType = ((ParameterizedType) genericType).getRawType();
            Object fieldV = null;
            field.setAccessible(true);
            try {
                fieldV = field.get(domain);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            if (Objects.isNull(fieldV)) {
                if (List.class.isAssignableFrom((Class<?>) rawType)) {
                    fieldV = new ArrayList<>();
                } else if (Set.class.isAssignableFrom((Class<?>) rawType)) {
                    fieldV = new HashSet<>();
                }
            } else {
                if (fieldV instanceof Collection) {
                    ((Collection) fieldV).clear();
                }
            }
            if (fieldV instanceof Collection) {
                Type[] actualTypeArguments = ((ParameterizedType) genericType).getActualTypeArguments();
                Type actualTypeArgument = actualTypeArguments[0];
                for (String id : ids) {
                    EmbeddedSelect annotation = field.getAnnotation(EmbeddedSelect.class);
                    Class<?> embeddedDomainClass = annotation.value();
                    String domainName = DomainMappingManager.MappingSupport.getDomainName(embeddedDomainClass);
                    Object embeddedObj = get(domainName, id);
                    Object embeddedFieldValue = null;
                    try {
                        embeddedFieldValue = ((Class<?>) actualTypeArgument).newInstance();
                    } catch (InstantiationException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                    BeanUtils.copyProperties(embeddedObj, embeddedFieldValue);
                    ((Collection) fieldV).add(embeddedFieldValue);
                }
            }
            try {
                field.set(domain, fieldV);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    private void onionItem(Object domain, Field field, String name) {
        Map<String, Object> params = ((AbstractPersistable) domain).getParams();
        Object embeddedId = null;
        if (Objects.nonNull(params)) {
            embeddedId = params.get(name);
            if (embeddedId instanceof Map) {
                embeddedId = ((Map) embeddedId).get("id");
            }
        } else {
            Object fieldVal = ReflectionUtils.getFieldVal(field, domain);
            Optional<Field> embeddedIdOptional = ReflectionUtils.listAllField(field.getType()).stream().filter(field1 -> field1.getName().equals("id")).findFirst();
            if (embeddedIdOptional.isPresent()) {
                embeddedId = ReflectionUtils.getFieldVal(embeddedIdOptional.get(), fieldVal);
            }
        }
        if (Objects.nonNull(embeddedId)) {
            EmbeddedSelect annotation = field.getAnnotation(EmbeddedSelect.class);
            Class<?> embeddedDomainClass = annotation.value();
            String domainName = DomainMappingManager.MappingSupport.getDomainName(embeddedDomainClass);
            Object embeddedObj = get(domainName, embeddedId.toString());
            try {
                Object embeddedFieldValue = field.getType().newInstance();
                BeanUtils.copyProperties(embeddedObj, embeddedFieldValue);
                field.setAccessible(true);
                field.set(domain, embeddedFieldValue);
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * @author yangrd
     * @date 2018/8/30
     **/
    static class BeanUtils {

        /**
         * 只拷贝不为null的属性
         *
         * @param source the source bean
         * @param target the target bean
         * @throws BeansException if the copying failed
         */
        public static void copyProperties(Object source, Object target) throws BeansException {
            List<Field> collectionFields = ReflectionUtils.listAllField(source.getClass()).stream().filter(field -> Collection.class.isAssignableFrom(field.getType())).collect(Collectors.toList());
            List<String> ignoreProperties = new ArrayList<>();
            String[] nullPropertyNames = getNullPropertyNames(source);
            for (String nullPropertyName : nullPropertyNames) {
                ignoreProperties.add(nullPropertyName);
            }
            collectionFields.forEach(field -> {
                /*try {
                    field.setAccessible(true);
                    Object o = field.get(target);
                    if (o != null) {
                        ((Collection) o).clear();
                        ((Collection) o).addAll((Collection) field.get(source));

                    }

                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }*/
                ignoreProperties.add(field.getName());
            });

            String[] a = new String[ignoreProperties.size()];
            String[] ts = ignoreProperties.toArray(a);
            org.springframework.beans.BeanUtils.copyProperties(source, target, ts);
        }

        private static String[] getNullPropertyNames(Object source) {
            final BeanWrapper src = new BeanWrapperImpl(source);
            PropertyDescriptor[] pds = src.getPropertyDescriptors();

            Set<String> emptyNames = new HashSet<>();
            for (PropertyDescriptor pd : pds) {
                Object srcValue = src.getPropertyValue(pd.getName());
                if (srcValue == null) {
                    emptyNames.add(pd.getName());
                }
            }
            String[] result = new String[emptyNames.size()];

            return emptyNames.toArray(result);
        }
    }

}
